DataFrame in Pandas

I dataframe sono delle strutture dati equivalenti alle tabelle di un database.

Al giorno d'oggi tutti i linguaggi di alto livello ne hanno un'implementazione.

Un dataframe è una tabella di dati indicizzata da righe e colonne.

il caso più semplice è una tabella che contanga informazioni a proposito di persone;

Ogni riga contiene un'indicativo unico della persona, ogni colonna rappresenta delle caratteristiche di quella persona.

In [4]:
import pandas as pd

data = pd.DataFrame([('Andrea', 24, 178, 'Maschio'),
                     ('Maria', 33, 154, 'Femmina'),
                     ('Luca', 30, 175, 'Maschio')],
                    columns=['nome', 'età', 'altezza', 'genere'])
data.set_index('nome', inplace=True)
In [5]:
data
Out[5]:
età altezza genere
nome
Andrea 24 178 Maschio
Maria 33 154 Femmina
Luca 30 175 Maschio
In [52]:
import pandas as pd

data = pd.DataFrame([('Andrea', '2015', 'residenza', 'Rimini', 'via stretta 20'),
                     ('Andrea', '2015', 'domicilio', 'Bologna', 'via larga 30'),
                     ('Andrea', '2016', 'residenza', 'Rimini', 'via stretta 20'),
                     ('Andrea', '2016', 'domicilio', 'Bologna', 'via larga 30'),
                     ('Giulio', '2015', 'residenza', 'Rimini', 'via giusta 50'),
                     ('Giulio', '2015', 'domicilio', 'Bologna', 'via falsa 40'),
                     ('Giulio', '2016', 'residenza', 'Bologna', 'via torna 10'),
                     ('Giulio', '2016', 'domicilio', 'Bologna', 'via torna 10'),
                    ], columns=['nome', 'anno', 'tipologia', 'città', 'indirizzo']
                    )
data.set_index(['nome', 'anno', 'tipologia'], inplace=True)
data = data.unstack()
data.columns = data.columns.swaplevel(0, 1)
data.sortlevel(0, axis=1, inplace=True)

Righe e colonne possono avere indici GERARCHICI, in cui ho più livelli di indicizzazione delle mie informazioni

In [53]:
data
Out[53]:
tipologia domicilio residenza
città indirizzo città indirizzo
nome anno
Andrea 2015 Bologna via larga 30 Rimini via stretta 20
2016 Bologna via larga 30 Rimini via stretta 20
Giulio 2015 Bologna via falsa 40 Rimini via giusta 50
2016 Bologna via torna 10 Bologna via torna 10

I dataframe permettono di raccogliere e manipolare le informazioni in modo particolarmente comodo, e sono il pilastro centrale della moderna analisi dati.

In particolare hanno una rilevanza centrale i dataframe strutturati come TIDY DATA, termine introdotto da Wickham nel 2010 (paper di spiegazione).

I tidy data sono un modo particolare di strutturare le tabelle che rende l'analisi ed il mantenimento dei dati particolarmente comodo.

La struttura tidy data che Wickham definisce è caratterizzata dalle seguenti proprietà:

  • Ogni variabili forma una colonna e contiene dei valori singoli
  • ogni osservazione forma una riga
  • ogni tipo di osservazione forma una tabella

Dove:

  • Variabile: una misura di un attributo (altezza, peso, genere, etc...)
  • Valore: la specifica misura ottenuta
  • Osservazione: tutti i valori misurati per una specifica unità, come una persona in un certo momento nel tempo

Corrisponde dal punto di vista formale alla terza forma normale dei database.

In [54]:
import pandas as pd

data = pd.DataFrame([('Andrea', '2015', 'residenza', 'Rimini', 'via stretta 20'),
                     ('Andrea', '2015', 'domicilio', 'Bologna', 'via larga 30'),
                     ('Andrea', '2016', 'residenza', 'Rimini', 'via stretta 20'),
                     ('Andrea', '2016', 'domicilio', 'Bologna', 'via larga 30'),
                     ('Giulio', '2015', 'residenza', 'Rimini', 'via giusta 50'),
                     ('Giulio', '2015', 'domicilio', 'Bologna', 'via falsa 40'),
                     ('Giulio', '2016', 'residenza', 'Bologna', 'via torna 10'),
                     ('Giulio', '2016', 'domicilio', 'Bologna', 'via torna 10'),
                    ], columns=['nome', 'anno', 'tipologia', 'città', 'indirizzo']
                    )
In [55]:
data
Out[55]:
nome anno tipologia città indirizzo
0 Andrea 2015 residenza Rimini via stretta 20
1 Andrea 2015 domicilio Bologna via larga 30
2 Andrea 2016 residenza Rimini via stretta 20
3 Andrea 2016 domicilio Bologna via larga 30
4 Giulio 2015 residenza Rimini via giusta 50
5 Giulio 2015 domicilio Bologna via falsa 40
6 Giulio 2016 residenza Bologna via torna 10
7 Giulio 2016 domicilio Bologna via torna 10

La cosa importante da ricordare con questo tipo di dati (e la approfondiremo meglio la prossima lezione) è che il formato di dati per fare storage non è necessariamente il più comodo per ogni possibile analisi.

Il formato tidy è eccezionale, sopratutto per mantenere i metadati sulle mie misure, ma non sempre è il formato più comodo per l'analisi che voglio fare (ad esempio differenze fra vari momenti temporali).

Per questo motivo, tutte le librerie che lavorano sui dataframe hanno un forte focus sulla trasformazione dei ati da una forma all'altra, per permetterci di passare facilmente alla struttura dati che serve alle analisi senza sacrificare la qualità dei dati in storage.

Introduzione a Pandas

Pandas è la libreria di python che permette la manipolazione dei dataframe.

La libreria introduce la classe DataFrame, che contiene una tabella, che contiene diverse Series, ovvero i dati in colonna che condividono lo stesso indice.

In [9]:
import pandas as pd
import numpy as np

Uno dei punti di forza di pandas è la capacità di leggere e scrivere praticamente qualsiasi dato di tipo tabulare.

Ad esempio possiamo caricare tutte le tabelle di una pagina wikipedia

In [8]:
page = 'https://en.wikipedia.org/wiki/List_of_highest-grossing_films'
wikitables = pd.read_html(page, attrs={"class":"wikitable"})
In [10]:
len(wikitables)
Out[10]:
83
In [11]:
wikitables[0].head()
Out[11]:
0 1 2 3 4 5
0 Rank Peak Title Worldwide gross Year Reference(s)
1 1 1 Avatar $2,787,965,087 2009 [# 1][# 2]
2 2 1 Titanic $2,186,772,302 1997 [# 3][# 4]
3 3 3 Star Wars: The Force Awakens $2,068,223,624 2015 [# 5][# 6]
4 4 3 Jurassic World $1,670,400,637 2015 [# 7][# 8]

Le funzioni di lettura contengono decine e decine di parametri per riuscire a leggere i dati esattamente come vogliamo.

In [74]:
wikitables = pd.read_html(page, attrs={"class":"wikitable"}, header=0)
wikitables[0].head()
Out[74]:
Rank Peak Title Worldwide gross Year Reference(s)
0 1 1 Avatar $2,787,965,087 2009 [# 1][# 2]
1 2 1 Titanic $2,186,772,302 1997 [# 3][# 4]
2 3 3 Star Wars: The Force Awakens $2,068,223,624 2015 [# 5][# 6]
3 4 3 Jurassic World $1,670,400,637 2015 [# 7][# 8]
4 5 3 The Avengers $1,518,812,988 2012 [# 9][# 10]
In [75]:
dataframe = wikitables[0].copy()

Il dataframe si comporta come una specie di dizionario.

Le chiavi sono le colonne, e ciascuna chiave fa riferimento alla Series lì contenuta

In [76]:
dataframe.columns
Out[76]:
Index(['Rank', 'Peak', 'Title', 'Worldwide gross', 'Year', 'Reference(s)'], dtype='object')
In [77]:
dataframe['Year'].head()
Out[77]:
0    2009
1    1997
2    2015
3    2015
4    2012
Name: Year, dtype: int64

Le Series si comportano come gli array di numpy per quanto riguarda la vettorizzazione, ma lo fanno informati dall'indice della serie invece che dall'ordine degli elementi

In [78]:
dataframe['Year'].head() * 100
Out[78]:
0    200900
1    199700
2    201500
3    201500
4    201200
Name: Year, dtype: int64
In [79]:
a = pd.Series([1, 2, 3], index=['a', 'b', 'c'])
a
Out[79]:
a    1
b    2
c    3
dtype: int64
In [80]:
b = pd.Series([5, 6, 7], index=['c', 'a', 'b'])
b
Out[80]:
c    5
a    6
b    7
dtype: int64
In [81]:
a+b
Out[81]:
a    7
b    9
c    8
dtype: int64

posso manipolare le mie colonne in molti modi, partendo dall'eliminazione di colonne non interessanti

In [82]:
del dataframe['Reference(s)']
In [83]:
dataframe.head()
Out[83]:
Rank Peak Title Worldwide gross Year
0 1 1 Avatar $2,787,965,087 2009
1 2 1 Titanic $2,186,772,302 1997
2 3 3 Star Wars: The Force Awakens $2,068,223,624 2015
3 4 3 Jurassic World $1,670,400,637 2015
4 5 3 The Avengers $1,518,812,988 2012
In [84]:
dataframe.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 50 entries, 0 to 49
Data columns (total 5 columns):
Rank               50 non-null object
Peak               50 non-null object
Title              50 non-null object
Worldwide gross    50 non-null object
Year               50 non-null int64
dtypes: int64(1), object(4)
memory usage: 2.0+ KB
In [85]:
dataframe['Title'].head()
Out[85]:
0                          Avatar
1                         Titanic
2    Star Wars: The Force Awakens
3                  Jurassic World
4                    The Avengers
Name: Title, dtype: object
In [86]:
dataframe.head()
Out[86]:
Rank Peak Title Worldwide gross Year
0 1 1 Avatar $2,787,965,087 2009
1 2 1 Titanic $2,186,772,302 1997
2 3 3 Star Wars: The Force Awakens $2,068,223,624 2015
3 4 3 Jurassic World $1,670,400,637 2015
4 5 3 The Avengers $1,518,812,988 2012

Spesso e volentieri i dati reali arrivano in un formato "non ottimale".

La prima parte di qualsiasi analisi consiste nella pulizia dei dati.

Nel nostro caso ad esempio potremmo voler aggiustare il guadagno, che al momento è visto come una stringa.

In [87]:
c = 'Worldwide gross'
dataframe[c] = dataframe[c].str.replace(',', '')
dataframe[c] = dataframe[c].str.replace('$', '')
dataframe[c] = dataframe[c].astype(int)
In [88]:
dataframe.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 50 entries, 0 to 49
Data columns (total 5 columns):
Rank               50 non-null object
Peak               50 non-null object
Title              50 non-null object
Worldwide gross    50 non-null int64
Year               50 non-null int64
dtypes: int64(2), object(3)
memory usage: 2.0+ KB

Per aggiustare i dati talvolta è necessario usare la violenza...

In [89]:
try:
    dataframe['Peak'].astype(int)
except ValueError as e:
    print(e)
invalid literal for int() with base 10: '4TS3'
In [97]:
print(list(dataframe['Peak'].unique()))
['1', '3', '4', '5', '10', '12', '2', '7', '4TS3', '20', '6', '22', '24', '14', '19DM2', '30', '26', '8FN', '8', '15', '40', '29', '47', '45']

non esiste una trasformazione semplice che possa convertire questo tipo di dati, quindi usiamo una regular expression

In [33]:
import re
regex = re.compile('(\d+)\D*\d*')
dataframe['Peak'] = dataframe['Peak'].str.extract(regex, expand=False)
dataframe['Peak'] = dataframe['Peak'].astype(int)
In [34]:
dataframe.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 50 entries, 0 to 49
Data columns (total 5 columns):
Rank               50 non-null object
Peak               50 non-null int64
Title              50 non-null object
Worldwide gross    50 non-null int64
Year               50 non-null int64
dtypes: int64(3), object(2)
memory usage: 2.0+ KB
In [35]:
dataframe['Rank'] = dataframe['Rank'].str.extract(regex, expand=False)
dataframe['Rank'] = dataframe['Rank'].astype(int)
In [36]:
dataframe.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 50 entries, 0 to 49
Data columns (total 5 columns):
Rank               50 non-null int64
Peak               50 non-null int64
Title              50 non-null object
Worldwide gross    50 non-null int64
Year               50 non-null int64
dtypes: int64(4), object(1)
memory usage: 2.0+ KB
In [37]:
dataframe.describe()
Out[37]:
Rank Peak Worldwide gross Year
count 50.00000 50.000000 5.000000e+01 50.00000
mean 25.50000 10.900000 1.131099e+09 2009.42000
std 14.57738 11.429018 3.661334e+08 6.07803
min 1.00000 1.000000 8.719397e+08 1993.00000
25% 13.25000 4.000000 9.399983e+08 2006.25000
50% 25.50000 6.000000 1.024626e+09 2011.00000
75% 37.75000 13.500000 1.122905e+09 2014.75000
max 50.00000 47.000000 2.787965e+09 2016.00000
In [38]:
%matplotlib inline

visualizzazione dei dataframe

Posso farlo sia da Pandas che da Matplotlib.

Dopo vedremo una libreria più appropriata, ma queste vanno bene per un approccio quick & dirty

In [39]:
import pylab as plt
In [40]:
plt.scatter('Rank', 'Peak', data=dataframe)
Out[40]:
<matplotlib.collections.PathCollection at 0x7f5ce8b80828>
In [41]:
dataframe.plot.scatter('Rank', 'Peak')
Out[41]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f5cac89aa20>
In [42]:
plt.plot('Rank', 'Worldwide gross', data=dataframe)
Out[42]:
[<matplotlib.lines.Line2D at 0x7f5cacb616d8>]
In [43]:
with plt.xkcd():
    dataframe.plot('Rank', 'Worldwide gross')
In [44]:
plt.hist('Worldwide gross', data=dataframe);
In [45]:
dataframe['Worldwide gross'].plot.hist()
Out[45]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f5cacc9a0f0>

pandas lavora in realtà con matplotlib, per cui potete estrarre il grafico al volo e modificarlo come volete

In [46]:
dataframe['Worldwide gross'].plot.hist()
ax = plt.gca()
ax.set_title("Histogram of Worldwide gross", fontsize=30)
Out[46]:
<matplotlib.text.Text at 0x7f5caca29c88>

DataFrames multidimensionali con XArray

I dataframe di Pandas (ed in generale la struttura del dataframe come concetto), contiene come dati in colonna soltanto dei valori scalari, come l'altezza, il peso, e così via.

Per gestire e manipolare dati più complessi, come ad esempio una serie temporale per ogni paziente, si devono spesso creare più tabelle e collegarle logicamente fra di loro.

Una alternativa piuttosto recente sono i DataArray e DataSet delle libreria XArray.

Questi permettono di creare delle strutture molto efficienti per gestire ad esempio immagini TAC dei pazienti in più dimensioni ed in più momenti temporali e manipolarle in modo sensato (per quanto questo possa essere possibile).

Operazioni sui DataFrame

Vediamo ora una serie di operazioni molto comuni sui dataframe.

  • Groupby
  • Join e Merge
  • Pivot, Melt, Stacking
  • Panel visualization con Seaborn

Groupby

Compie operazioni di tipo split-apply-combine:

  1. divide i dati in gruppi
  2. applica una trasformazione (mapping o aggregazione) ai dati
  3. unisce il risultato dei vari gruppi
In [98]:
wiki = "https://it.wikipedia.org/wiki/"
url_popolazione = wiki+"Comuni_d%27Italia_per_popolazione"
url_superficie = wiki+"Primi_100_comuni_italiani_per_superficie"
In [99]:
comuni_popolazione = pd.read_html(url_popolazione,
                                  attrs={"class":"wikitable"},
                                  header=0)
comuni_popolazione = comuni_popolazione[0]
comuni_popolazione.head()
Out[99]:
# Comune Regione Provincia / Città metropolitana Abitanti
0 1 Roma Lazio Roma 2 867 078
1 2 Milano Lombardia Milano 1 350 487
2 3 Napoli Campania Napoli 972 212
3 4 Torino Piemonte Torino 889 600
4 5 Palermo Sicilia Palermo 671 531
In [100]:
comuni_superficie = pd.read_html(url_superficie,
                                 attrs={"class":"wikitable"},
                                 header=0)
comuni_superficie = comuni_superficie[0]
comuni_superficie.head()
Out[100]:
Pos. Comune Regione Provincia Superficie (km²)
0 1 Roma Lazio Roma 128736
1 2 Ravenna Emilia-Romagna Ravenna 65382
2 3 Cerignola Puglia Foggia 59393
3 4 Noto Sicilia Siracusa 55499
4 5 Sassari Sardegna Sassari 54704
In [104]:
comuni_superficie.groupby('Regione').mean()
Out[104]:
Pos. Superficie (km²)
Regione
Abruzzo 9.000000 47391.000000
Basilicata 54.000000 29641.000000
Calabria 63.000000 26078.500000
Emilia-Romagna 55.400000 31154.100000
Lazio 28.750000 56263.750000
Liguria 79.000000 24029.000000
Lombardia 93.500000 22701.500000
Marche 83.333333 24168.000000
Puglia 45.526316 33032.631579
Sardegna 66.111111 29028.333333
Sicilia 45.600000 32300.150000
Toscana 51.357143 29941.357143
Trentino-Alto Adige 55.000000 27485.000000
Umbria 31.714286 36175.285714
Veneto 48.333333 30853.000000
In [111]:
g = comuni_superficie.groupby('Regione')
g.aggregate([np.mean, np.std, pd.Series.count])
Out[111]:
Pos. Superficie (km²)
mean std count mean std count
Regione
Abruzzo 9.000000 NaN 1 47391.000000 NaN 1
Basilicata 54.000000 32.787193 3 29641.000000 8419.030110 3
Calabria 63.000000 24.041631 2 26078.500000 3075.207391 2
Emilia-Romagna 55.400000 30.492986 10 31154.100000 13146.217719 10
Lazio 28.750000 24.185050 4 56263.750000 48688.754926 4
Liguria 79.000000 NaN 1 24029.000000 NaN 1
Lombardia 93.500000 2.121320 2 22701.500000 40.305087 2
Marche 83.333333 24.542480 3 24168.000000 2632.717987 3
Puglia 45.526316 29.279057 19 33032.631579 10050.061521 19
Sardegna 66.111111 32.811753 9 29028.333333 10824.258624 9
Sicilia 45.600000 27.013447 20 32300.150000 9656.570331 20
Toscana 51.357143 26.304744 14 29941.357143 7114.648125 14
Trentino-Alto Adige 55.000000 24.041631 2 27485.000000 3877.773588 2
Umbria 31.714286 20.361027 7 36175.285714 9897.267396 7
Veneto 48.333333 28.884829 3 30853.000000 9300.741315 3
In [106]:
comuni_superficie.groupby('Regione')['Superficie (km²)'].count()
Out[106]:
Regione
Abruzzo                 1
Basilicata              3
Calabria                2
Emilia-Romagna         10
Lazio                   4
Liguria                 1
Lombardia               2
Marche                  3
Puglia                 19
Sardegna                9
Sicilia                20
Toscana                14
Trentino-Alto Adige     2
Umbria                  7
Veneto                  3
Name: Superficie (km²), dtype: int64
In [103]:
g = comuni_superficie.groupby('Regione')['Superficie (km²)']
g.count().sort_values(ascending=False)
Out[103]:
Regione
Sicilia                20
Puglia                 19
Toscana                14
Emilia-Romagna         10
Sardegna                9
Umbria                  7
Lazio                   4
Veneto                  3
Marche                  3
Basilicata              3
Trentino-Alto Adige     2
Lombardia               2
Calabria                2
Liguria                 1
Abruzzo                 1
Name: Superficie (km²), dtype: int64
In [91]:
g = comuni_popolazione.groupby('Regione')['Abitanti']
g.count().sort_values(ascending=False)
Out[91]:
Regione
Campania                 19
Lombardia                15
Sicilia                  15
Puglia                   15
Toscana                  13
Emilia-Romagna           13
Lazio                    11
Veneto                    6
Piemonte                  6
Calabria                  5
Abruzzo                   5
Liguria                   4
Sardegna                  4
Marche                    3
Umbria                    3
Friuli-Venezia Giulia     3
Trentino-Alto Adige       2
Basilicata                2
Name: Abitanti, dtype: int64

Join e Merge

Quando ho due tabelle distinte, che condividono una chiave, posso fare il join fra le due.

Questo mi permette di tenere le tabelle dei miei dati in forma corretta (tidy e scorrelata) e ricrearle in modo comodo per le analisi combinando più tabelle insieme

In [121]:
a = pd.DataFrame([('Antonio', 'M'),
                  ('Marco', 'M'),
                  ('Francesca', 'F'),
                 ], columns = ['nome', 'genere'])

b = pd.DataFrame([('Antonio', 15),
                  ('Marco', 10),
                  ('Marco', 12),
                  ('Marco', 23),
                  ('Francesca', 20),
                 ], columns = ['nome', 'spesa'])
In [122]:
a
Out[122]:
nome genere
0 Antonio M
1 Marco M
2 Francesca F
In [123]:
b
Out[123]:
nome spesa
0 Antonio 15
1 Marco 10
2 Marco 12
3 Marco 23
4 Francesca 20
In [124]:
pd.merge(a, b, on='nome')
Out[124]:
nome genere spesa
0 Antonio M 15
1 Marco M 10
2 Marco M 12
3 Marco M 23
4 Francesca F 20
In [113]:
(
pd.merge(comuni_popolazione,
         comuni_superficie,
         on=['Comune', 'Regione'])
).head()
Out[113]:
# Comune Regione Provincia / Città metropolitana Abitanti Pos. Provincia Superficie (km²)
0 1 Roma Lazio Roma 2 867 078 1 Roma 128736
1 6 Genova Liguria Genova 585 208 79 Genova 24029
2 11 Venezia Veneto Venezia 262 344 15 Venezia 41590
3 16 Taranto Puglia Taranto 200 385 70 Taranto 24986
4 18 Parma Emilia-Romagna Parma 194 152 61 Parma 26060

Ci sono diversi modi di fare il merge, che corrispondono alle diverse combinazioni di insiemi possibili di chiavi.

sono controllati dal parametro HOW.

In [99]:
len(pd.merge(comuni_popolazione, comuni_superficie,
             on='Comune', how='right'))
Out[99]:
100
In [100]:
len(pd.merge(comuni_popolazione, comuni_superficie,
             on='Comune', how='left'))
Out[100]:
144
In [101]:
len(pd.merge(comuni_popolazione, comuni_superficie,
             on='Comune', how='inner'))
Out[101]:
38
In [102]:
len(pd.merge(comuni_popolazione, comuni_superficie,
             on='Comune', how='outer'))
Out[102]:
206

Pivot, melt, stacking

il pivoting permette di creare tavole riassuntive (incluse tavole di contingenza) a partire da una dataset tidy.

date due colonne che faranno da indice e nomi delle colonne del dataset risultante, si scelgono uno o più valori di cui fare il sommario (somma, media, conteggi, deviazione standard, etc...)

In [145]:
spese = [('Antonio', 'gatto', 4),
         ('Antonio', 'gatto', 5),
         ('Antonio', 'gatto', 6),
         ('Giulia', 'gatto', 3),
         ('Giulia', 'cane', 7),
         ('Giulia', 'cane', 8),
         
        ]

spese = pd.DataFrame(spese, columns = ['nome', 'animale', 'spesa'])
spese
Out[145]:
nome animale spesa
0 Antonio gatto 4
1 Antonio gatto 5
2 Antonio gatto 6
3 Giulia gatto 3
4 Giulia cane 7
5 Giulia cane 8
In [146]:
pd.pivot_table(spese,
               index='nome',
               columns='animale',
               values='spesa',
               aggfunc=np.sum)
Out[146]:
animale cane gatto
nome
Antonio NaN 15.0
Giulia 15.0 3.0
In [147]:
pd.pivot_table(spese,
               index='nome',
               columns='animale',
               values='spesa',
               aggfunc=np.sum,
               fill_value=0)
Out[147]:
animale cane gatto
nome
Antonio 0 15
Giulia 15 3
In [148]:
pd.pivot_table(spese,
               index='nome',
               columns='animale',
               values='spesa',
               aggfunc=np.sum,
               fill_value=0,
               margins=True)
Out[148]:
animale cane gatto All
nome
Antonio 0.0 15.0 15.0
Giulia 15.0 3.0 18.0
All 15.0 18.0 33.0
In [149]:
pd.pivot_table(spese,
               index='nome',
               columns='animale',
               values='spesa',
               aggfunc=pd.Series.count,
               fill_value=0)
Out[149]:
animale cane gatto
nome
Antonio 0 3
Giulia 2 1
In [166]:
r = pd.pivot_table(spese,
               index='nome',
               columns='animale',
               values='spesa',
               aggfunc=pd.Series.count,
               fill_value=0)
r = r.reset_index()
r
Out[166]:
animale nome cane gatto
0 Antonio 0 3
1 Giulia 2 1
In [170]:
v = pd.melt(r, id_vars=['nome'], value_vars=['cane', 'gatto'])
v
Out[170]:
nome animale value
0 Antonio cane 0
1 Giulia cane 2
2 Antonio gatto 3
3 Giulia gatto 1
In [176]:
v2 = v.set_index(['nome', 'animale'])['value']
v2
Out[176]:
nome     animale
Antonio  cane       0
Giulia   cane       2
Antonio  gatto      3
Giulia   gatto      1
Name: value, dtype: int64
In [180]:
v2.unstack()
Out[180]:
animale cane gatto
nome
Antonio 0 3
Giulia 2 1
In [182]:
v2.unstack().stack()
Out[182]:
nome     animale
Antonio  cane       0
         gatto      3
Giulia   cane       2
         gatto      1
dtype: int64
In [186]:
v.pivot(index='nome', columns='animale', values='value')
Out[186]:
animale cane gatto
nome
Antonio 0 3
Giulia 2 1
v.pivot(index='nome', columns='animale', values='value')

è identico a

v2.unstack()

ma uno agisce sulle serie (unstack) e l'altro sui dataframe di tipo tidy (pivot)

pandas + matplotlib = seaborn

Visualizzazione con i Panel

In [126]:
url_amminoacidi = 'https://en.wikipedia.org/wiki/Proteinogenic_amino_acid'
info_amminoacidi = pd.read_html(url_amminoacidi, attrs={"class":"wikitable"}, header=0)
In [127]:
len(info_amminoacidi)
Out[127]:
6
In [128]:
info_amminoacidi[0].head()
Out[128]:
Amino acid Short Abbrev. Avg. mass (Da) pI pK1 (α-COOH) pK2 (α-+NH3)
0 Alanine A Ala 89.09404 6.01 2.35 9.87
1 Cysteine C Cys 121.15404 5.05 1.92 10.70
2 Aspartic acid D Asp 133.10384 2.85 1.99 9.90
3 Glutamic acid E Glu 147.13074 3.15 2.10 9.47
4 Phenylalanine F Phe 165.19184 5.49 2.20 9.31
In [129]:
info_amminoacidi[1].head()
Out[129]:
Amino acid Short Abbrev. Side chain Hydro- phobic pKa§ Polar pH Small Tiny Aromatic or Aliphatic van der Waals volume (Å3)
0 Alanine A Ala -CH3 X - - - X X Aliphatic 67.0
1 Cysteine C Cys -CH2SH X 8.55 - acidic X X - 86.0
2 Aspartic acid D Asp -CH2COOH - 3.67 X acidic X - - 91.0
3 Glutamic acid E Glu -CH2CH2COOH - 4.25 X acidic - - - 109.0
4 Phenylalanine F Phe -CH2C6H5 X - - - - - Aromatic 135.0
In [130]:
tavola1 = info_amminoacidi[0]
tavola2 = info_amminoacidi[1]
del tavola2['Short']
del tavola2['Abbrev.']
tavola = pd.merge(tavola1, tavola2, on='Amino acid')
In [131]:
tavola.head()
Out[131]:
Amino acid Short Abbrev. Avg. mass (Da) pI pK1 (α-COOH) pK2 (α-+NH3) Side chain Hydro- phobic pKa§ Polar pH Small Tiny Aromatic or Aliphatic van der Waals volume (Å3)
0 Alanine A Ala 89.09404 6.01 2.35 9.87 -CH3 X - - - X X Aliphatic 67.0
1 Cysteine C Cys 121.15404 5.05 1.92 10.70 -CH2SH X 8.55 - acidic X X - 86.0
2 Aspartic acid D Asp 133.10384 2.85 1.99 9.90 -CH2COOH - 3.67 X acidic X - - 91.0
3 Glutamic acid E Glu 147.13074 3.15 2.10 9.47 -CH2CH2COOH - 4.25 X acidic - - - 109.0
4 Phenylalanine F Phe 165.19184 5.49 2.20 9.31 -CH2C6H5 X - - - - - Aromatic 135.0
In [132]:
tavola.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 22 entries, 0 to 21
Data columns (total 16 columns):
Amino acid                   22 non-null object
Short                        22 non-null object
Abbrev.                      22 non-null object
Avg. mass (Da)               22 non-null float64
pI                           21 non-null float64
pK1 (α-COOH)                 21 non-null float64
pK2 (α-+NH3)                 21 non-null float64
Side chain                   22 non-null object
Hydro- phobic                22 non-null object
pKa§                         22 non-null object
Polar                        22 non-null object
pH                           22 non-null object
Small                        22 non-null object
Tiny                         22 non-null object
Aromatic or Aliphatic        22 non-null object
van der Waals volume (Å3)    20 non-null float64
dtypes: float64(5), object(11)
memory usage: 2.9+ KB
In [133]:
tavola['Hydro- phobic'].unique()
Out[133]:
array(['X', '-'], dtype=object)
In [134]:
import seaborn

Seaborn di default cambia la configurazione stardard della visualizzazione di matplotlib.

Potete impostarla come preferite grazie al modulo styles di matplotlib.

In [49]:
from matplotlib import style
print(sorted(style.available))
style.use('default')
['bmh', 'classic', 'dark_background', 'fivethirtyeight', 'ggplot', 'grayscale', 'seaborn', 'seaborn-bright', 'seaborn-colorblind', 'seaborn-dark', 'seaborn-dark-palette', 'seaborn-darkgrid', 'seaborn-deep', 'seaborn-muted', 'seaborn-notebook', 'seaborn-paper', 'seaborn-pastel', 'seaborn-poster', 'seaborn-talk', 'seaborn-ticks', 'seaborn-white', 'seaborn-whitegrid']
In [153]:
seaborn.lmplot('Avg. mass (Da)',
               'van der Waals volume (Å3)',
               data=tavola,
               hue='Hydro- phobic')
Out[153]:
<seaborn.axisgrid.FacetGrid at 0x7f3c4bc0fa58>
In [154]:
seaborn.lmplot('Avg. mass (Da)',
               'van der Waals volume (Å3)',
               data=tavola,
               col='Hydro- phobic')
Out[154]:
<seaborn.axisgrid.FacetGrid at 0x7f3c4b5991d0>
In [155]:
seaborn.lmplot('Avg. mass (Da)',
               'van der Waals volume (Å3)',
               data=tavola,
               row='Hydro- phobic')
Out[155]:
<seaborn.axisgrid.FacetGrid at 0x7f3c496f1c18>
In [156]:
fg = seaborn.FacetGrid(data=tavola,
                       hue='Hydro- phobic',
                       size=6)
fg.map(plt.scatter,
       'Avg. mass (Da)',
       'van der Waals volume (Å3)',
       s=50)
fg.map(seaborn.regplot,
       'Avg. mass (Da)',
       'van der Waals volume (Å3)',
       scatter=False)
fg.add_legend();
In [157]:
fg = seaborn.FacetGrid(data=tavola,
                       col='Hydro- phobic',
                       hue='Hydro- phobic',
                       size=8,
                       aspect=.5)
fg.map(plt.scatter,
       'Avg. mass (Da)',
       'van der Waals volume (Å3)',
       s=50)
fg.map(seaborn.regplot,
       'Avg. mass (Da)',
       'van der Waals volume (Å3)',
       scatter=False)
fg.add_legend();
In [158]:
columns = ['Avg. mass (Da)',
           'van der Waals volume (Å3)',
           'pI', 'pK1 (α-COOH)',
           'pK2 (α-+NH3)']
tavola[columns].head()
Out[158]:
Avg. mass (Da) van der Waals volume (Å3) pI pK1 (α-COOH) pK2 (α-+NH3)
0 89.09404 67.0 6.01 2.35 9.87
1 121.15404 86.0 5.05 1.92 10.70
2 133.10384 91.0 2.85 1.99 9.90
3 147.13074 109.0 3.15 2.10 9.47
4 165.19184 135.0 5.49 2.20 9.31
In [159]:
fd = tavola[columns+['Hydro- phobic']].dropna()
g = seaborn.PairGrid(df,
                     size=2,
                     hue='Hydro- phobic')
g.map_diag(plt.hist, alpha=0.5)
g.map_offdiag(plt.scatter, alpha=0.75, s=20);

Esercizio

Esaminando la tabella di wikipedia dei premi nobel in fisica, classificare quali paese abbiamo avuto il maggior numero di nobel nel corso degli anni

https://en.wikipedia.org/wiki/List_of_Nobel_laureates_in_Physics

Per i più coraggiosi, provate a correlare la lista precendente con il consumo pro capite di birra.

https://en.wikipedia.org/wiki/List_of_countries_by_beer_consumption_per_capita

Soluzione all'esercizio

Purtroppo mi sono accorto solo a posteriori che la tabella dei nobel aveva delle anomalie:

  • in caso di vincitori multipli, la tabella era formattata in modo strano che portava le linee a compattarsi insieme e shiftare in modo strano
  • alcuni vincitori hanno la doppia cittadinanza, rendendo più difficile l'attribuzione del premio ad una sola nazione.

Il secondo problema si può risolvere facilmente con la decisione arbitraria di conteggiare soltanto la prima cittadinanza (la nazione di nascita, secondo quanto riportato dal sito del premio nobel).

Il primo richiede la manipolazione delle righe del dataframe, che mostro di seguito.

Come vedrete la manipolazione non è particolarmente complicata, ma richiede conoscenze che non vi ho mostrato a lezione, mi spiace.

In [114]:
# carico il dataset incriminato
import pandas as pd
wiki = 'https://en.wikipedia.org/wiki/'
url = wiki+'List_of_Nobel_laureates_in_Physics'
dfs = pd.read_html(url,
                   attrs={"class":"wikitable"},
                   header=0)
df = dfs[0].copy()
df.head()
Out[114]:
Year Laureate[A] Country[B] Rationale[C] Unnamed: 4
0 1901.0 NaN Wilhelm Conrad Röntgen Germany "in recognition of the extraordinary services ...
1 1902.0 NaN Hendrik Lorentz Netherlands "in recognition of the extraordinary service t...
2 NaN Pieter Zeeman Netherlands NaN NaN
3 1903.0 NaN Antoine Henri Becquerel France "for his discovery of spontaneous radioactivit...
4 NaN Pierre Curie France "for their joint researches on the radiation p... NaN
In [115]:
# L'anno ha dei buchi dove ci sono più vincitori
# posso riempire i buchi conuna funzione 'forward-fill'
# che li riempe con il valore immediatamente precedente
df['Year'] = y.fillna(method='ffill').astype(int)
df.head()
Out[115]:
Year Laureate[A] Country[B] Rationale[C] Unnamed: 4
0 1901 NaN Wilhelm Conrad Röntgen Germany "in recognition of the extraordinary services ...
1 1902 NaN Hendrik Lorentz Netherlands "in recognition of the extraordinary service t...
2 1902 Pieter Zeeman Netherlands NaN NaN
3 1903 NaN Antoine Henri Becquerel France "for his discovery of spontaneous radioactivit...
4 1903 Pierre Curie France "for their joint researches on the radiation p... NaN
In [117]:
# la parte difficile.
# per prima cosa devo fare la correzione una riga alla volta
for idx, line in df.iterrows():
    # se il vincitore di quella riga è assente
    # devo fare la correzione
    missing_winner = pd.isnull(line['Laureate[A]'])
    if missing_winner:
        # creo una nuova tupla di valori ricombinando 
        # quella vecchia nell'ordine corretto
        new_value = line[['Year', 'Country[B]', 'Rationale[C]']]
        
        # riempo le prime 3 colonne con i valori che ho trovato
        # l'argomento values mi serve a fargli ignorare 
        # l'indicizzazione esplicita della serie
        df.loc[idx, :3] = new_value.values
df.head()
Out[117]:
Year Laureate[A] Country[B] Rationale[C] Unnamed: 4
0 1901 Wilhelm Conrad Röntgen Germany Germany "in recognition of the extraordinary services ...
1 1902 Hendrik Lorentz Netherlands Netherlands "in recognition of the extraordinary service t...
2 1902 Pieter Zeeman Netherlands NaN NaN
3 1903 Antoine Henri Becquerel France France "for his discovery of spontaneous radioactivit...
4 1903 Pierre Curie France "for their joint researches on the radiation p... NaN
In [118]:
# rendiamo il dataframe un po' più piacevole allo sguardo
del df['Rationale[C]']
del df['Unnamed: 4']
df.columns = ['year', 'name', 'country']
df.head()
Out[118]:
year name country
0 1901 Wilhelm Conrad Röntgen Germany
1 1902 Hendrik Lorentz Netherlands
2 1902 Pieter Zeeman Netherlands
3 1903 Antoine Henri Becquerel France
4 1903 Pierre Curie France
In [119]:
# controlliamo i valori unici della variabile country.
# possiamo vedere che dove c'è la doppia cittadinanza 
# abbiamo un carattere speciale ('\xa0')
# che possiamo usare per dividerle
df['country'].unique()
Out[119]:
array(['Germany', 'Netherlands', 'France', 'Poland \xa0France',
       'United Kingdom', 'Austria-Hungary \xa0Germany',
       'United States \xa0Poland', 'Italy', 'Sweden',
       'Australia \xa0United Kingdom', nan, 'Switzerland',
       'Germany  Switzerland', 'Denmark', 'United States', 'India',
       'Austria', 'Japan', 'Ireland', 'Switzerland \xa0United States',
       'West Germany', 'United States \xa0Germany',
       'China \xa0United States', 'Soviet Union',
       'Hungary \xa0United States', 'Hungary \xa0United Kingdom',
       'United States \xa0Norway', 'Pakistan',
       'Netherlands \xa0United States', 'India \xa0United States',
       'Canada', 'France \xa0Poland', 'Russia', 'Italy \xa0United States',
       'Russia \xa0United States', 'United Kingdom \xa0United States',
       'Japan \xa0United States',
       'Hong Kong \xa0United Kingdom \xa0United States',
       'Canada \xa0United States',
       'Russia \xa0United Kingdom \xa0Netherlands',
       'Russia \xa0United Kingdom', 'Australia \xa0United States',
       'Belgium'], dtype=object)
In [120]:
# divido la stringa in due dove trovo il carattere speciale
t = df['country'].str.split('\xa0')

# prendo il primo pezzo di stringa
t = t.str[0]

# tolgo gli spazi bianchi
t = t.str.strip()

# la assegno di nuovo alla variabile country
df['country'] = t
In [121]:
# alcuni anni non hanno il country
# sono quelli in cui il nobel 
# non è stato assegnato per via delle guerre
df.iloc[19:22]
Out[121]:
year name country
19 1915 William Lawrence Bragg Australia
20 1916 Not awarded World War I NaN
21 1917 Charles Glover Barkla United Kingdom
In [122]:
# posso rimuovere le linee in cui il country è assente
df = df.dropna(subset=['country'])
In [123]:
# posso infine fare il mio groupby 
# e vedere il risultato ordinato
gb = df.groupby('country')['year']
gb.count().sort_values(ascending=False)
Out[123]:
country
United States           76
United Kingdom          22
Germany                 14
France                  12
Japan                   11
West Germany             9
Netherlands              9
Soviet Union             7
Russia                   5
Italy                    5
Switzerland              4
Sweden                   4
Canada                   4
Austria                  3
China                    3
Denmark                  3
India                    2
Hungary                  2
Australia                2
Germany  Switzerland     1
Hong Kong                1
Ireland                  1
Pakistan                 1
Belgium                  1
Austria-Hungary          1
Poland                   1
Name: year, dtype: int64

Anche se l'esercizio era più difficile del previsto, sappiate che queste situazione capitano più spesso del previsto.

Ogni volta che avrete dei dati "curati" a mano, preparatevi a trovare ogni qual sorta di strani errori ed inconsistenze, che dovrete raddrizzare a colpi di codice.

In queste cose, Pandas è insostituibile (e questa è la ragione principale del suo successo)